/*
 * Licensed to the Tai Ping Jin Ke
 *
 * Copyright (c) 2022 .
 * All rights reserved.
 * 项目名称：太平金科-基础组件封装层-会话管理-核心依赖
 * 版权说明：本软件属太平金融科技服务(上海)有限公司所有，在未获得太平金融科技服务(上海)有限公司正式授权情况下，任何企业和个人，不能获取、阅读、安装、传播本软件涉及的任何受知识产权保护的内容。
 */
package com.taipingframework.boot.session;

import com.taipingframework.boot.cache.core.CommonCacheHandler;
import com.taipingframework.boot.cache.redis.DynamicCacheKeyBuilder;
import com.taipingframework.boot.cache.redis.StandardCacheKeyBuilder;
import com.taipingframework.boot.session.constant.SessionCacheKeyEnum;
import com.taipingframework.boot.session.constant.SessionCachePathEnum;
import com.taipingframework.boot.session.constant.SessionConstant;
import com.taipingframework.boot.session.support.ClientDetails;
import com.taipingframework.boot.web.client.ServletCookieScene;
import com.taipingframework.boot.web.response.status.ApiStatusEnum;
import com.taipingframework.boot.web.response.status.ExceptionStatusEnum;
import com.taipingframework.utility.constant.ApplicationConstant;
import com.taipingframework.utility.constant.SystemConstant;
import com.taipingframework.utility.exception.SystemInterrupterException;
import com.taipingframework.utility.exception.UtilityException;
import com.taipingframework.utility.thread.threadlocal.TransmittableThreadLocalHelper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.lang.NonNull;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

/**
 * HttpSession会话控制
 */
@Slf4j
public class HttpSessionManager {

    /**
     * 标准缓存(读/写)处理器
     */
    private static CommonCacheHandler cacheHandler;

    public HttpSessionManager(CommonCacheHandler cacheHandler) throws UtilityException {
        HttpSessionManager.cacheHandler = cacheHandler;
        // 在每次应用启动时，刷新一次缓存中的数据，防止配置变更后无法及时读取到正确的配置信息
        cacheHandler.setValue(SessionCacheKeyEnum.SERVER_SESSION_PATH_INFO,
                DynamicCacheKeyBuilder.createUniqueCacheKey(SessionConstant.PLACEHOLDER_SESSION_ID, SessionCachePathEnum.SESSION_DATA_PATH));
    }

    /**
     * 刷新当前账号在缓存服务器上面的会话数据
     *
     * @param loginName       登录名
     * @param clientIpAddress 终端显示层所在设备的IP
     */
    public static void refreshTerminalSession(String loginName, String clientIpAddress, String flagManagedProfile) throws UtilityException {
        try {
            // 刷新当前会话对象数据
            String uniqueTokenKey = buildUniqueTokenKey(loginName, clientIpAddress, flagManagedProfile);
            cacheHandler.expireByDeduce(uniqueTokenKey, SessionCachePathEnum.SESSION_UNEXPIRED_PATH);
            // 刷新当前账号在缓存服务器上面的终端用户空间列表数据
            cacheHandler.expireByDeduce(clientIpAddress, SessionCachePathEnum.SESSION_PROFILE_PATH);
            // 刷新当前账号在缓存服务器上面的会话对象列表数据
            cacheHandler.expireByDeduce(loginName, SessionCachePathEnum.SESSION_CREDENTIALS_PATH);
        } catch (Exception e) {
            throw new UtilityException(e);
        }
    }

    /**
     * 移除缓存对象在缓存服务器上面失效的缓存数据，并返回有效的缓存数据
     * <p>
     * 如果登录账号允许在多个终端同时登录，则该账号在缓存服务器上面会存在多个会话对象。
     * 另外，同一个账号在多个不同终端同时登录后，创建的多个会话对象之间，因为Spring-Session的会话过期机制在多个会话对象间是彼此隔离的，
     * 所以，它们在缓存服务器上面的会话数据也是必须进行逻辑隔离的。
     *
     * @param dynamicCacheKey 登录名或者终端Ip
     * @param cachePathEnum   缓存对象的路径：client-credentials对应的入参`dynamicCacheKey`应为登录名；client-profile对应的入参`dynamicCacheKey`应为终端IP
     * @return cachePathEnum参数为client-credentials对应返回的是Map<会话对象唯一标识, 终端ip>；cachePathEnum参数为client-profile对应返回的是Map<终端用户空间标识, 用户名>
     */
    public static Map<String, String> removeExpiredTerminalSession(@NonNull String dynamicCacheKey, @NonNull SessionCachePathEnum cachePathEnum) throws UtilityException {
        try {
            // 获取当前会话对象存放在缓存服务器的缓存数据
            Map<String, String> hashMap = cacheHandler.getHash(dynamicCacheKey, cachePathEnum);
            // 搜集登录过期的缓存数据
            List<String> expiredHashKeyList = new ArrayList<>();
            for (Map.Entry<String, String> entry : hashMap.entrySet()) {
                if (cachePathEnum == SessionCachePathEnum.SESSION_CREDENTIALS_PATH) {
                    // 基于WebSession与HttpSession两种方式实现的spring-session框架，在使用时的差异：
                    // spring-web场景下使用spring-session在注销之后，redis服务器上缓存的会话数据会延时5分钟失效。
                    // 因此，后端在执行注销操作后我们需要使会话相关的缓存数据同步失效。于是，此处不需要担心误判的情况发生！
                    if (cacheHandler.hasKey(entry.getKey(), SessionCachePathEnum.SESSION_EXPIRES_PATH)) {
                        // 筛选过期的缓存数据
                        continue;
                    }
                    // 返回过期的hashKey列表：client-credentials对应返回的是会话对象唯一标识列表
                    expiredHashKeyList.add(entry.getKey());
                } else if (cachePathEnum == SessionCachePathEnum.SESSION_PROFILE_PATH) {
                    String uniqueTokenKey = buildUniqueTokenKey(entry.getValue(), dynamicCacheKey, entry.getKey());
                    if (cacheHandler.hasKey(uniqueTokenKey, SessionCachePathEnum.SESSION_UNEXPIRED_PATH)) {
                        // 筛选过期的缓存数据
                        continue;
                    }
                    // 返回过期的hashKey列表：client-profile对应返回的是终端用户空间标识列表
                    expiredHashKeyList.add(entry.getKey());
                } else {
                    // 在后续维护过程中，一定要明确设计思路，入参错误此处必抛异常
                    throw new SystemInterrupterException(ApiStatusEnum.ERR_A0410);
                }
            }

            if (expiredHashKeyList.size() > 0) {
                // 移除当前账号在缓存服务器上面失效的登录凭证
                for (String hashKey : expiredHashKeyList) {
                    hashMap.remove(hashKey);
                }
                cacheHandler.removeHashEntry(dynamicCacheKey, cachePathEnum, expiredHashKeyList);
            }

            return hashMap;
        } catch (Exception e) {
            throw new UtilityException(e);
        }
    }

    /**
     * 在缓存中添加/更新验证信息
     *
     * @param clientDetails 客户端发送HTTP请求的参数详细
     * @param sessionId     会话对象标识
     */
    static void saveOrUpdateCurrentTerminalSession(ClientDetails clientDetails, String sessionId) throws UtilityException {
        try {
            String loginName = clientDetails.getClientLoginName();
            String clientIpAddress = clientDetails.getClientIpAddress();
            String flagManagedProfile = clientDetails.getClientUserProfile();

            // 将当前会话对象标识记录在缓存服务器，用于说明当前会话对象是有效的
            cacheHandler.setValue(buildUniqueTokenKey(loginName, clientIpAddress, flagManagedProfile),
                    SessionCachePathEnum.SESSION_UNEXPIRED_PATH, sessionId);
            // 在缓存服务器记录当前账号在终端的用户空间信息
            cacheHandler.putHash(clientIpAddress, SessionCachePathEnum.SESSION_PROFILE_PATH, flagManagedProfile, loginName);
            // 在缓存服务器记录当前账号登录成功后创建的会话对象标识
            cacheHandler.putHash(loginName, SessionCachePathEnum.SESSION_CREDENTIALS_PATH, sessionId, clientIpAddress);
        } catch (Exception e) {
            throw new UtilityException(e);
        }
    }

    /**
     * 删除验证信息
     */
    static void deleteCurrentTerminalSession(String loginName, String clientIpAddress, String flagManagedProfile, String sessionId) throws UtilityException {
        try {
            cacheHandler.delete(buildUniqueTokenKey(loginName, clientIpAddress, flagManagedProfile), SessionCachePathEnum.SESSION_UNEXPIRED_PATH);
            cacheHandler.removeHashEntry(clientIpAddress, SessionCachePathEnum.SESSION_PROFILE_PATH, Collections.singletonList(flagManagedProfile));
            cacheHandler.removeHashEntry(loginName, SessionCachePathEnum.SESSION_CREDENTIALS_PATH, Collections.singletonList(sessionId));
        } catch (Exception e) {
            throw new UtilityException(e);
        }
    }

    /**
     * 读取当前账号通过当前终端设备在缓存服务器创建的会话对象标识
     */
    static String readCurrentTerminalSession(String loginName, String clientIpAddress, String flagManagedProfile) throws UtilityException {
        try {
            return Optional.ofNullable(cacheHandler.getValue(buildUniqueTokenKey(loginName, clientIpAddress, flagManagedProfile),
                    SessionCachePathEnum.SESSION_UNEXPIRED_PATH)).orElse(StringUtils.EMPTY);
        } catch (Exception e) {
            throw new UtilityException(e);
        }
    }

    /**
     * 校验当前登录名在缓存服务器是否已经存在会话对象数据
     * <p>
     * PS：虽然登录接口被权限验证模块加入白名单，这样使得登录接口在被调用时不需要提供Access-Token，但是如果进行严谨的逻辑验证：
     * 在会话对象的有效期内，一个账户的会话对象必须是唯一的，它在客户端的凭证也必须是唯一的，既不能被租借也防止被盗取！
     * <p>
     * 实际上，根据Spring-Session框架的特点，无论是在请求头中还是在Cookie中，每次HTTP请求只要可以拿到一个有效的Access-Token，
     * 那么当前HTTP请求处理线程肯定已经被绑定在了某个唯一的会话对象之下的，因此，保护会话对象的安全，就是保护业务数据的安全！
     * <p>
     * 另外，在基于传统Servlet原生Session框架中，由于会话对象在分布式集群架构下都是独立的，想要实现会话对象的唯一性，一般都会通过
     * 会话共享、会话复制、会话保持三种方式达到目的，但性能不佳，也跟不上互联网技术发展的步伐，基本已经不用。
     *
     * @param clientDetails 客户端发送HTTP请求的参数详细
     * @return true - 表示需要读取当前会话对象并返回；false - 表示需要创建会话对象并返回
     */
    static boolean validateClientInfo(ClientDetails clientDetails) throws UtilityException {
        try {
            return StringUtils.isNotEmpty(ClientDetails.validateForSession(clientDetails));
        } catch (Exception e) {
            throw new UtilityException(e);
        }
    }

    /**
     * 读取客户端发送HTTP请求的参数详细
     */
    public static ClientDetails readClientDetails(HttpServletRequest request, String loginName) throws UtilityException {
        try {
            // 当前HTTP请求的唯一标识
            final String clientRequestId = Optional.ofNullable(TransmittableThreadLocalHelper.getEnvironment().get(ApplicationConstant.RESPONSE_IDENTITY))
                    .orElse(StringUtils.EMPTY);
            // 获取HTTP请求的远程客户端设备标识
            String clientIpAddress = Optional.ofNullable(TransmittableThreadLocalHelper.getEnvironment().get(ApplicationConstant.REMOTE_CLIENT_IP_KEY))
                    .orElse(StringUtils.EMPTY);
            // 获取请求头中的token
            String requestHeaderToken = Optional.ofNullable(request.getHeader(StandardCacheKeyBuilder.getAuthTokenKey()))
                    .orElse(StringUtils.EMPTY);

            // 装载会话管理模块功能支撑详细参数
            ClientDetails clientDetails = new ClientDetails();
            clientDetails.setClientRequestId(clientRequestId);
            clientDetails.setClientIpAddress(clientIpAddress);
            clientDetails.setRequestHeaderToken(requestHeaderToken);
            clientDetails.setClientLoginName(loginName);

            String clientUserProfile = readClientUserProfile(request);
            String clientPresentToken = Optional.ofNullable(cacheHandler.getValue(buildUniqueTokenKey(loginName, clientIpAddress, clientUserProfile),
                    SessionCachePathEnum.SESSION_UNEXPIRED_PATH)).orElse(StringUtils.EMPTY);
            clientDetails.setClientUserProfile(clientUserProfile);
            clientDetails.setClientPresentToken(clientPresentToken);

            Map<String, String> serverUserProfileMap = removeExpiredTerminalSession(clientIpAddress, SessionCachePathEnum.SESSION_PROFILE_PATH);
            clientDetails.setServerUserProfileMap(serverUserProfileMap);

            Map<String, String> serverCredentialsMap = removeExpiredTerminalSession(loginName, SessionCachePathEnum.SESSION_CREDENTIALS_PATH);
            clientDetails.setServerCredentialsMap(serverCredentialsMap);

            HttpSession httpSession = request.getSession(false);
            if (Objects.nonNull(httpSession)) {
                clientDetails.setSessionPrincipal(Optional.ofNullable((String) httpSession.getAttribute(SessionConstant.SESSION_LOGIN_KEY))
                        .orElse(StringUtils.EMPTY));
                clientDetails.setSessionClientIpAddress(Optional.ofNullable((String) httpSession.getAttribute(ApplicationConstant.REMOTE_CLIENT_IP_KEY))
                        .orElse(StringUtils.EMPTY));
                clientDetails.setSessionUserProfile(Optional.ofNullable((String) httpSession.getAttribute(SessionConstant.CLIENT_USER_PROFILE_KEY))
                        .orElse(StringUtils.EMPTY));
            }

            return clientDetails;
        } catch (Exception e) {
            throw new UtilityException(e);
        }
    }

    /**
     * 构建HTTP会话对象的属性名
     */
    @NonNull
    static String buildAttributeCacheKey(String hashName) {
        return String.join(SystemConstant.CACHE_SEPARATOR, SessionConstant.PREFIX_SESSION_ATTRIBUTE, hashName);
    }

    /**
     * 用户会话标识的缓存对象命名规则
     */
    static String buildUniqueTokenKey(String loginName, String clientIpAddress, String clientUserProfile) {
        return String.join(SystemConstant.TITLE_SEPARATOR, clientIpAddress, loginName, clientUserProfile);
    }

    /**
     * 读取客户端发送HTTP请求的用户空间
     */
    public static String readClientUserProfile(HttpServletRequest request) throws UtilityException {
        try {
            String flagManagedProfile = request.getHeader(SessionConstant.MANAGED_PROFILE_KEY);
            if (StringUtils.isEmpty(flagManagedProfile)) {
                flagManagedProfile = ServletCookieScene.readCookie(SessionConstant.MANAGED_PROFILE_KEY).getValue();
            }
            if (StringUtils.isEmpty(flagManagedProfile) || StringUtils.isBlank(flagManagedProfile)) {
                throw new SystemInterrupterException(ExceptionStatusEnum.UNABLE_GET_MANAGED_PROFILE);
            }
            return flagManagedProfile;
        } catch (Exception e) {
            throw new UtilityException(e);
        }
    }

}
